package ULHT.LIG2N1.RC.SERVER;

import ULHT.LIG2N1.RC.VO.BlacklistVO;
import ULHT.LIG2N1.RC.VO.FileManage;
import ULHT.LIG2N1.RC.VO.QuestionLimitVO;
import ULHT.LIG2N1.RC.VO.WhitelistVO;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.*;
import java.security.SecureRandom;
import java.util.Locale;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

class ClientThread implements Runnable {

    private Socket clientTCPsocket;
    private DatagramSocket UDPSocket;
    private InetAddress UDPclientAddress;
    private int UDPclientPort;
    private GameVariables gameVariables;
    private String clientID;
    private BufferedReader in;
    private PrintWriter out;
    private SecureRandom tokenSeed = new SecureRandom();
    private String token;
    private boolean erro = false;
    private BlacklistVO blacklist = new BlacklistVO();
    private WhitelistVO whitelist = new WhitelistVO();
    private GameLogicHAndler game = new GameLogicHAndler(whitelist, blacklist);
    private boolean running = true;
    private FileManage filemanager = new FileManage();
    private String sessionFileName = "session.dat";

    ClientThread(Socket server, GameVariables gameVariables) {
        this.clientTCPsocket = server;
        try {
            this.UDPSocket = new DatagramSocket();
        } catch (SocketException ex) {
            System.out.println("Erro no socket UDP");
            halt();
        }
        this.UDPclientAddress = clientTCPsocket.getInetAddress();

        this.UDPclientPort = 9031;
        System.out.println(UDPSocket.getPort());
        this.gameVariables = gameVariables;

        this.clientID = this.clientTCPsocket.getInetAddress().getHostAddress();
        this.token = nextSessionId();

        try {
            in = new BufferedReader(new InputStreamReader(server.getInputStream()));
            out = new PrintWriter(clientTCPsocket.getOutputStream(), true);
        } catch (IOException ex) {
            System.out.println("Erro nos buffers de leitura e escrita TCP");
            halt();
        }

        ClientGameData clientGameData = new ClientGameData(this.clientID, this.token, this.UDPclientAddress.toString(), this.UDPclientPort, "", 0);
        this.gameVariables.addClient(clientGameData);

        System.out.println("UDP IP  : " + this.UDPclientAddress.toString());
        System.out.println("UDP Port: " + this.UDPclientPort);
        System.out.println("ID CLI  : " + this.clientID);
    }

    @Override
    public void run() {
        //TODO ver esta lógica do forbidden
        while (running) {
            String opcaoCliente = null;
            if (!game.isIPAllowed(clientTCPsocket.getInetAddress().getHostAddress())) {
                writeToClient("FORBIDDEN");
            } else {
                try {
                    do {
                        opcaoCliente = readFromClient();
                        if (erro) {
                            return;
                        }
                        switch (opcaoCliente.toUpperCase()) {
                            case "HANDSHAKE":
                                if (!game.isIPAllowed(clientTCPsocket.getInetAddress().getHostAddress())) {
                                    writeToClient("FORBIDDEN");
                                } else {
                                    writeToClient("HELLO");
                                    writeToClient(token);
                                }
                                break;
                            case "REJOIN":
                                handleReconnect();
                                break;
                            case "WHITELIST":
                                writeToClient(gameVariables.getWhiteList().getWhiteListStr());
                                break;
                            case "PLAYERS":
                                writeToClient(gameVariables.getPlayersListStr());
                                break;
                            case "BLACKLIST":
                                writeToClient(gameVariables.getBlackList().getBlackListStr());
                                break;
                            case "TIME":
                                writeToClient("Hora do Servidor: " + getServerTime());
                                break;
                            case "NEXTGAME":
                                writeToClient("Hora de Inicio do próximo jogo: " + this.gameVariables.getStringHoraInicioProximoJogo() + "   ");
                                break;
                            case "PLAY":
                                handleQuestion();
                                break;
                            case "QUIT":
                                writeToClient("BYE");
                                clientTCPsocket.close();
                                UDPSocket.close();
                                break;
                            case "TESTUNICAST":
                                sendDisqualifyNotice();
                                break;
                            case "HELP":
                                sendHelp();
                                break;
                            case "PING":
                                writeToClient("PONG");
                                break;
                            case "CHECKCONNECTION":
                                writeToClient("OK");
                                break;
                            default:
                                writeToClient("ERR");
                        }
                    } while (!opcaoCliente.equalsIgnoreCase("QUIT"));
                    // fim do bloco de logica do servidor TCP
                } catch (IOException ioe) {
                    System.out.println("IOException no socket TCP");
                    this.erro = true;
                    halt();
                }
            }
        }
    }

    public void halt() {
        this.running = false;
    }

    private void sendHelp() {
        String helpStr;

        helpStr = "*** Comandos do servidor ***\n\n";
        helpStr += "HANDSHAKE - inicio de sessao\n";
        helpStr += "            responde com HELLO e token caso seja valido ou\n";
        helpStr += "            responde com FORBIDDEN caso seja blacklisted\n";
        helpStr += "REJOIN    - ligar a uma sessão iniciada previamente\n";
        helpStr += "            responde com GRANTED caso seja valido ou\n";
        helpStr += "            responde com FORBIDDEN caso seja blacklisted\n";
        helpStr += "WHITELIST - Lista de participantes inscritos (whitelist)\n";
        helpStr += "PLAYERS   - Lista de participantes online\n";
        helpStr += "BLACKLIST - Lista de participantes desclassificados (blacklist)\n";
        helpStr += "TIME      - Hora do servidor\n";
        helpStr += "HELP      - Esta lista\n";
        helpStr += "QUIT      - terminar sessao\n";
        helpStr += "ERR       - Comando desconhecido ou fora de contexto";
        helpStr += "PING      - Responde com PONG, utilizado para verificar se a ligação existe\n";
        helpStr += "PLAY      - iniciar a sessão de jogo\n";
        helpStr += "            * Responde com WAIT e mensagem de quando se inicia a próxima sessao, OU:\n";
        helpStr += "            1. SENDQUESTIONTIMEOUT ao qual o cliente deve responder \"READY\"\n";
        helpStr += "            2. envia o timeout máximo da resposta em jogo (int)\n";
        helpStr += "            3. fica a aguardar \"GETQUESTION\" do cliente\n";
        helpStr += "            4. responde com STARTANSWERSESSION ao qual o cliente deve responder \"READY\"\n";
        helpStr += "            5. Envia a pergunta e aguarda a resposta do cliente\n";
        helpStr += "            6. Envia CERTO ou ERRADO e o tempo da resposta\n";
        helpStr += "            7. Volta a 1 até ao fim do jogo, onde envia GAMEEND\n\n";
        helpStr += "               ou à desqualificacao, onde envia DISQUALIFIED e GAMEEND.\n";
        helpStr += "               Depois de GAMEEND, envia as estatisticas do Jogo ao cliente\n\n";
        helpStr += "*** FIM DA AJUDA ***";
        writeToClient(helpStr);
    }

    public void sendDisqualifyNotice() {
        String response = "Desqualificado!";
        DatagramPacket outPacket = new DatagramPacket(response.getBytes(), response.length(), this.UDPclientAddress, this.UDPclientPort);
        try {
            this.UDPSocket.send(outPacket);
        } catch (IOException ex) {
            System.out.println("Erro no escrita UDP");
            halt();
        }
    }

    public String getServerTime() {
        DateTimeFormatter fmt = DateTimeFormat.mediumTime();
        return DateTime.now().toString(fmt.withLocale(Locale.FRANCE));
    }

    private String nextSessionId() {
        return new BigInteger(130, tokenSeed).toString(32);
    }

    private String readFromClient() {
        String clientStream = null;

        try {
            clientStream = in.readLine();
            return clientStream.toUpperCase();
        } catch (IOException | NullPointerException ex) {
            this.erro = true;
            System.out.println("Erro na leitura do socket!\nCliente desligado");
            halt();
        }
        return null;
    }

    private void writeToClient(String output) {
        try {
            if (!clientTCPsocket.isClosed()) {
                System.out.printf("\nmsglen : %d\nmsg: %s\n", output.length(), output);
                out.println(output.length());
                out.println(output);
            }
        } catch (NullPointerException npe) {
            halt();
        }
    }

    private void handleQuestion() {
        ClientGameData gameData;
        String resposta, feedback;
        boolean isCorrect;

        if (gameVariables.isPlayable()) {
            gameVariables.getClients().get(token).initGameTimers();
            if (gameVariables.getClients().get(token).getCurrentLevel() < 1) {
                gameVariables.getClients().get(token).levelUp();
            }

            for (int i = gameVariables.getClients().get(token).getQuestionNumber(); i <= 15; i++) {
                gameVariables.getClients().get(token).setState("INGAME");
                //---
                if (!gameVariables.getClients().get(token).isDisqualified()) {
                    writeToClient("SENDQUESTIONTIMEOUT");
                    if (readFromClient().equals("READY")) {
                        System.out.println("lvl  : " + gameVariables.getClients().get(token).getCurrentLevel());
                        System.out.println("ssend: " + gameVariables.getClients().get(token).getTimeTillSessionEnd());
                        System.out.println("tout : " + (gameVariables.getClients().get(token).getCurrentLevel() * QuestionLimitVO.getquestionTimeout(i)) * 1000);

                        long timeout = gameVariables.getClients().get(token).getCurrentLevel() == 4 ? gameVariables.getClients().get(token).getTimeTillSessionEnd() : (gameVariables.getClients().get(token).getCurrentLevel() * QuestionLimitVO.getquestionTimeout(i) * 1000);
                        System.out.println(Long.toString(timeout));

                        System.out.println(
                                " Nivel " + gameVariables.getClients().get(token).getCurrentLevel()
                                + " | Pergunta " + gameVariables.getClients().get(token).getQuestionNumber()
                                + " | Certas no nivel : " + gameVariables.getClients().get(token).getCorrectAnswersInLevel()
                                + " | Pontos : " + gameVariables.getClients().get(token).getScore()
                                + " | Tempo total : " + gameVariables.getClients().get(token).getTotalTimeStr());

                        writeToClient(Long.toString(timeout));
                        gameVariables.getClients().get(token).advanceState();
                        //---

                        if (readFromClient().equals("GETQUESTION")) {
                            writeToClient("STARTANSWERSESSION");
                            if (readFromClient().equals("READY")) {
                                //guarda o time de inicio
                                gameVariables.getClients().get(token).startAnswering(gameVariables.getClients().get(token).getQuestionNumber());
                                //envia a pergunta
                                writeToClient(gameVariables.getGameQuestions().get(gameVariables.getClients().get(token).getQuestionNumber() - 1).strToClient());
                                gameVariables.getClients().get(token).advanceState();
                                //---


                                //recebe a resposta à pergunta
                                resposta = readFromClient();

                                // guarda o time da resposta
                                gameVariables.getClients().get(token).StopAnswering();

                                // verifica se a resposta está correcta e devolve o resultado 
                                isCorrect = gameVariables.checkAnswer(gameVariables.getClients().get(token).getQuestionNumber(), resposta);

                                // calcula o tempo da resposta (e incrementa o contador caso tenha sido correcta)
                                gameVariables.getClients().get(token).calculateAnswerTime(isCorrect, QuestionLimitVO.getquestionTimeout(i), QuestionLimitVO.getquestionScore(i));

                                if (isCorrect) {
                                    feedback = "CERTO! ";
                                } else {
                                    feedback = "ERRADO. ";
                                }
                                writeToClient(feedback + "Tempo de resposta: " + Float.toString((float) (gameVariables.getClients().get(token).getLastAnswerTime() / 1000)) + " Segundos");
                                gameVariables.getClients().get(token).advanceState();
                                //--
                                //verifica o estado da ronda e incrementa os contadores
                                if (i <= 14) {
                                    if (QuestionLimitVO.getquestionLevel(i + 1) != gameVariables.getClients().get(token).getCurrentLevel()) {
                                        if (gameVariables.getClients().get(token).getCorrectAnswersInLevel() < 1) {
                                            // desqualificado
                                            writeToClient("DISQUALIFIED");
                                            sendDisqualifyNotice();
                                            game.blacklistClient(gameVariables.getClients().get(token).getIpAdress());
                                            gameVariables.getClients().get(token).makeBlacklisted();
                                            gameVariables.getClients().get(token).Disqualify();
                                            break;
                                        }
                                    }
                                    gameVariables.getClients().get(token).nextQuestion();
                                    try {
                                        filemanager.writeGameVariablesToFile(gameVariables, "session.dat");
                                    } catch (IOException ex) {
                                        System.out.println("ERRO AO ESCREVER A SESSÃO NO DISCO!");
                                    }
                                }
                            } else {
                                writeToClient("ERR");
                                return;
                            }
                        } else {
                            writeToClient("ERR");
                            return;
                        }
                    } else {
                        writeToClient("ERR");
                        return;
                    }
                }
            }
            writeToClient("GAMEEND");
            writeToClient(
                    "Total de perguntas: " + gameVariables.getClients().get(token).getQuestionNumber()
                    + ", Correctas: " + gameVariables.getClients().get(token).getTotalCorrectAnswers()
                    + ", Erradas: " + (gameVariables.getClients().get(token).getQuestionNumber() - gameVariables.getClients().get(token).getTotalCorrectAnswers())
                    + ", Pontos: " + gameVariables.getClients().get(token).getScore()
                    + ", Tempo: " + gameVariables.getClients().get(token).getTotalTimeStr());

            gameVariables.getClients().get(token).resetState();


        } else {
            writeToClient("WAIT");
            writeToClient("Não está nenhuma pergunta em jogo, são " + getServerTime() + " e o próximo jogo inicia-se às " + gameVariables.getStringTimeTillNextGame() + "   ");
        }
    }

    private void handleReconnect() {
        this.token = readFromClient();
        switch (gameVariables.getClients().get(token).getState()) {
            case "IDLE":
                break;
            case "INGAME":
        }
    }
}
